#-*- coding: utf-8 -*-

import logging
import re
import sys
import urllib

from datetime import datetime
from datetime import date
from google.appengine.api import mail
from google.appengine.ext import db
from lxml.html import fromstring

class Encode:
    '''
    Helper class to remvoe encode method for unicode characters.
    Without this class, we need to do u'안녕하세요'.encode('utf-8')
    '''
    def __init__(self, stdout, enc):
        self.stdout = stdout
        self.encoding = enc

    def write(self, s):
        ''' Override stdout write'''
        if self.is_ascii(s):
            self.stdout.write(s.encode(self.encoding))
        else:
            self.stdout.write(s)

    def is_ascii(self, s):
        return all(ord(c) < 128 for c in s)


# Override standard output
sys.stdout = Encode(sys.stdout, 'utf-8')


class Account(db.Model):
    '''
    Account object to save the authenticated users
    '''

    # User object from the Google Auth system
    login = db.UserProperty()

    # First name of the Account holder
    first_name = db.StringProperty()

    # Last name of the Account holder
    last_name = db.StringProperty()

    # Status of the Account
    status = db.StringProperty()

    # Alternative email to get the notification
    primary_email = db.EmailProperty(required=False)

    # Create date of Account
    create_date = db.DateTimeProperty(auto_now_add=True)

    # Modify date of Account
    modify_date = db.DateTimeProperty(auto_now=True)


class WatchItem(db.Model):
    '''
    This model stores the item information to watch
    '''

    # The name of the site or other target, e.g. 'My iPhone watchlist'
    name = db.StringProperty()

    # Target information to watch, e.g. http://illinoisksa.com/board/1
    url = db.StringProperty()

    # Keyword to search, e.g. iPhone
    keyword = db.StringProperty(required=True)

    account = db.ReferenceProperty(Account,
                                   collection_name='watch_items')

    enabled = db.BooleanProperty(default=True, indexed=False)

    # Start Date to watch the item
    start_date = db.DateProperty()

    # End Date to watch the item
    end_date = db.DateProperty()

    # Create date of Account
    create_date = db.DateTimeProperty(auto_now_add=True)

    # Modify date of Account
    modify_date = db.DateTimeProperty(auto_now=True)


    # Create a WatchItem.notifier simulating the bi-directional
    # relationship
    @property
    def notifier(self):
        if not hasattr(self, '_notifier_cached'):
            setattr(self, '_notifier_cached', self.notifier_set.get())
        return self._notifier_cached


class Posting(db.Model):
    '''
    This model stores the posting of the board.
    '''

    # Number of the posting
    number = db.IntegerProperty()

    # Title of the posting
    title = db.StringProperty()

    # Author of the posting
    author = db.StringProperty()

    # Date of the posting
    time = db.DateProperty()

    # How many times the posting was read
    read_num = db.IntegerProperty()

    # Content of the posting
    content = db.TextProperty()
    
    # Time that posting was added to the database
    create_date = db.DateTimeProperty(auto_now_add=True)

    
class WebContent(db.Model):
    '''
    Temporary space to store the html content to replace the cache file
    '''
    
    # Html content retrieved by the application
    html_content = db.TextProperty()
    
    # Modify date of Account
    modify_date = db.DateTimeProperty(auto_now=True)


class Notifier(db.Model):
    '''
    This model stores the information about the notification to the user on the
    watch item.
    '''

    # Start Date to begin notification
    start_date = db.DateProperty

    # End Date to end notification
    end_date = db.DateProperty

    # Medium type to notify
    media = db.StringProperty(required=True,
                        choices=set(["email","text"]))

    # Set the one to one relationship with WatchItem entity
    watch_item = db.ReferenceProperty(WatchItem)


class WebCrawler(object):
    """ Web content crawler to parse the watch item posting. """

    def __init__(self):
        pass

    def fetch(self, url):
        '''
        Function to fetch the bulletin board and add the new postings to db
        '''
        
        content = urllib.urlopen(url).read()
        doc = fromstring(content)
        doc.make_links_absolute(url)
        postings = doc.find_class('board_list')[0].xpath('//tr')

        most_recent_posting = Posting.gql("ORDER BY number DESC limit 1").get()

        for p in postings:
            if len(p.xpath('td')) > 0:
                number = p.xpath('td')[0].text_content().strip()

                # if number is not "공지"
                if len(number) > 2:
                    try:
                        posting = Posting()
                        posting.number = int(number)

                        # Clean up the line break
                        posting.title = \
                            re.sub(r'\s*\n\s*', ' ',
                                   p.xpath('td')[1].text_content().strip())
                        posting.author = \
                            p.xpath('td')[2].text_content().strip()
                        posting.time = datetime.strptime(
                            p.xpath('td')[3].text_content().strip(),
                            "%Y.%m.%d").date()
                        posting.read_num = \
                            int(p.xpath('td')[4].text_content().strip())
                        posting.content = \
                            re.sub(r'\s*\n\s*', ' ',
                                self.fetch_posting_content(
                                p.xpath('td')[1].getchildren()[0].values()[0]))

                        logging.info("title: %s, author: %s" %
                                     (posting.title, posting.author))
                        # Fetch only the new posting: compare the most recent
                        # posting and the posting to save and skip saving if
                        # the posting is old one.
                        q = Posting.all()

                        # Remove the reply number,i.e. [1] from the title
                        regex = re.compile(r"\[\d*\]")
                        title = regex.sub(r"",posting.title).rstrip()
                        logging.debug(
                            "Title after trimming reply and white space %s" % 
                            title)
                        q.filter("title =", title).\
                            filter("author =", posting.author)
                        if q.count() > 0:
                            logging.info(
                        "Posting %d to save already exists in the database."
                        % posting.number)
                            return True

                        # Save the posting
                        posting.put()
                        logging.info("New posting with title %s is saved." % \
                            posting.title)

                        # Send the notification email to user for the
                        # new posting
                        q = WatchItem.all()
                        q.filter("start_date <=", date.today())

                        # Filter the watch item with start/end date and
                        # keyword 
                        for p in q.run():
                            # Compose the re pattern
                            if p.keyword:
                                pattern = r'.*' + p.keyword + '.*'
                            else:
                                continue

                            if p.end_date >= date.today() and \
                              (re.match(pattern, posting.title, re.I)\
                              or \
                              re.match(pattern, posting.content, re.I)):
                                logging.info(
                            "Sending email to %s for the keyword, %s" %
                            (p.account.primary_email, p.keyword))

                                self.send_mail(p, posting)

                    except Exception as e:
                        logging.error("Error while adding a new posting: %s" %
                                      e.message)
                        pass

        return False


    def fetch_posting_content(self, url):
        '''
        Function to fetch the content of posting and return
        '''

        content = urllib.urlopen(url).read()
        doc = fromstring(content)
        doc.make_links_absolute(url)

        body = doc.find_class('read_body')[0].xpath('div')[0]
        
        logging.debug("Posting body fetch: %s" % body)
        return body.text_content()


    def fetch_all(self):
        '''
        Function to check if the most recent posting in the database exists
        in the list of postings of the board.
        It will call fetch function if it is not the case.
        '''

        # If there is no posting in the database, fetch the entry page
        if Posting.all().count() == 0:
            logging.debug("No posting entry exists in database and fetching" +\
                        "for the first time.")
            self.fetch("http://illinoisksa.org/xe/fleamarket")
            return

        # If the most recent posting in the database can't be found from
        # the list of the postings, move on to the next page and query.
        url_prefix = 'http://illinoisksa.org/xe/index.php?mid=fleamarket&page='

        # Fetch the first 5 posting pages
        for counter in range(1, 6):
            # If the fetched posting has the most recent posting of the db,
            # stop the fetching.
            result = self.fetch(''.join([url_prefix, str(counter)]))
            if result:
                logging.debug("Exiting the loop to fetch 5 pages.")
                return


    def send_mail(self, watch_item, posting):
        '''
        Function to send an email
        '''
        # Now send mail to the user
        if watch_item.account.first_name or watch_item.account.last_name:
            receiver_name = "%s %s" % (watch_item.account.first_name,
                                  watch_item.account.last_name)
        else:
            receiver_name = watch_item.account.primary_email

        message = mail.EmailMessage(
            sender="Trade 16 <support@trade16.com>",\
            subject="Watch Item Notification: %s" % posting.title)

        message.to = watch_item.account.primary_email
        message.body = \
          ("Dear %s, \n\nWatch Item with keyword, \"%s\" is posted. " +
            "\n\nPost Title: \n%s" +
            "\n\nPost Content: \n%s" +
            "\n\n\nThanks, \n\nTrade 16 team") \
            % (receiver_name, watch_item.keyword, posting.title,
               posting.content)

        message.send()
        logging.info("Sent the email to %s with keyword, %s" % \
                (receiver_name, watch_item.keyword))



